
var language = ( function() {

    function mergeScores(scoresA, scoresB) {
        var result = {};

        //copy A a as merge destination (result)
        for (var aKey in scoresA) {
            result[aKey] = {
                score       : scoresA[aKey].score,
                impressions : scoresA[aKey].impressions
            }
        }

        //walk through B merging into destination (result)
        for (var srcScoreItemKey in scoresB) {
            var srcScoreItem = scoresB[srcScoreItemKey];
            if (!(srcScoreItemKey in result)) {
                //destination doesn't have this score key
                result[srcScoreItemKey] = {
                    score       : srcScoreItem.score,
                    impressions : srcScoreItem.impressions
                }
            } else {
                var dstScoreItem = result[srcScoreItemKey];
                var combinedImpressions = srcScoreItem.impressions+dstScoreItem.impressions;
                dstScoreItem = {
                    impressions : combinedImpressions,
                    score       : ((srcScoreItem.impressions*srcScoreItem.score)+(dstScoreItem.impressions*dstScoreItem.score))/combinedImpressions
                }
            }
        }

        return result;
    }

    var PersistentLanguage = ( function() {
        /*
        A single PersistentLanguage is a languages.Storage specialisation
        and hence it's given a folder location with a manifest.json file in it.
        This manifest.json describes all available resources for a single language
        and it is expected to look like this:

        Each parcel[i] is an
        */
        function PersistentLanguage(fs, rootFolder, sessionId) {
            this.mCachedCuts = {};
            languages.Storage.call(this, fs, rootFolder, sessionId);
        }
        PersistentLanguage.prototype.__proto__ = languages.Storage.prototype;

        PersistentLanguage.prototype.name = function() {
            if (this.mManifests.personal.manifest.name) {
                return this.mManifests.personal.manifest.name;
            }
            if (this.mManifests.personalRemote &&
                this.mManifests.personalRemote.manifest &&
                this.mManifests.personalRemote.manifest.name) {
                return this.mManifests.personalRemote.manifest.name;
            }
            if (this.mManifests.global &&
                this.mManifests.global.manifest &&
                this.mManifests.global.manifest.name) {
                return this.mManifests.global.manifest.name;
            }
            return "Unknown";
        }

        PersistentLanguage.prototype.displayName = function() {
            return this.name();
        }


        PersistentLanguage.prototype.init = function() {
            var self = this;
            var result = $.Deferred();
            languages.Storage.prototype.init.call(this)
            .then(function() {
                console.log("PersistentLanguage["+self.name()+"].load(), merging remote->local");
                return merge(self, self.mManifests.personalRemote, self.mManifests.personal, true);
            })
            .then(function() {
                console.log("PersistentLanguage["+self.name()+"].load(), merged remote->local, merging global->local");
                return merge(self, self.mManifests.global, self.mManifests.personal, false)
            })
            .then(function() {
                return self.save();
            })
            .then(function() {
                result.resolve();
            });
            return result.promise();
        }

        PersistentLanguage.prototype.save = function(onlyLocal) {
            if (this.mDirty && this.mManifests.personal.fs) {
                console.log("PersistentLanguage["+this.name()+"].save(), found dirty, saving..");
                delete this["mDirty"];

                var tasks = [];
                for (var cutName in this.mCachedCuts) {
                    var cut = this.mCachedCuts[cutName];
                    for (var i = 0; i < cut.parcels.length; i++) {
                        var parcel = cut.parcels[i];
                        tasks.push(parcel.save(onlyLocal));
                    }
                }

                var path = this.path("manifest.json");
                tasks.push(this.mManifests.personal.fs.write(path, JSON.stringify(this.mManifests.personal.manifest)));

                if (this.mManifests.personalRemote && !onlyLocal) {
                    this.mManifests.personalRemote.manifest = JSON.parse(JSON.stringify(this.mManifests.personal.manifest));
                    tasks.push(this.mManifests.personalRemote.fs.write(path, JSON.stringify(this.mManifests.personal.manifest)));
                }
                return when.all(tasks);

            }
            return $.Deferred().resolve().promise();
        }


        PersistentLanguage.prototype.getParcel = function(cutName, parcelIndex) {
            var self = this;
            var result = $.Deferred();
            if (cutName in this.mCachedCuts && this.mCachedCuts[parcelIndex]) {
                return $Deferred.resolve(this.mCachedCuts[cutName].parcels[parcelIndex]).promise();
            }

            var newParcel = new Parcel(this, cutName, parcelIndex);
            return newParcel.load()
            .pipe(function() {
                if (!(cutName in self.mCachedCuts)) {
                    self.mCachedCuts[cutName] = {
                        parcels : []
                    };
                }

                for (var i = 0; i <= parcelIndex; i++) {
                    if (i == parcelIndex) {
                        self.mCachedCuts[cutName].parcels.push(newParcel);
                    }
                    else if (i > self.mCachedCuts[cutName].parcels.length) {
                        self.mCachedCuts[cutName].parcels.push(null);
                    }
                }
                return newParcel;
            });
        }

        function merge(self, src, dst, and) {
            /*
            Format for the src or local (a manifest item)
            {
                "fs"         : file system from which this file was read from (and can be optional saved to)
                "manifest" {
                    o"timestamp" : <long>       - global timestamp (when global manifest was produced or, on what global was a personal based)
                    o"sessionId" : <long>       - personal manifests only, which session last wrote to this manifest
                    o"name"      : l"Italian"   - name of the language this manifest pertains to
                    o"cuts" : {                 - different snapshots/gropings of this dictionary
                        l"verbs_nouns" : {
                            "parcels : ...
                        },
                        ...
                    }
                }
            }
            */

            var tasks = [];
            if (src && dst && src.manifest && dst.manifest) { //there is anything to merge to/from
                debug.assert(!dst.manifest.timestamp || dst.manifest.name, "manifest never written, yet non-empty");
                if (!dst.manifest.timestamp || //no timestamp on destination (nobody every written to it, means its empty)
                    src.manifest.sessionId && src.manifest.sessionId != dst.manifest.sessionId || //different sessions wrote to manifests
                    src.manifest.timestamp > dst.manifest.timestamp) { //src is newer than dest
                    dst.manifest.name = src.manifest.name;
                    dst.manifest.timestamp = src.manifest.timestamp;
                    if (!dst.manifest.cuts) {
                        dst.manifest.cuts = {}
                    }
                    for (var cutName in src.manifest.cuts) {
                        if (cutName != 'all') {
                            tasks.push(mergeCut(self, cutName, src, dst, and));
                        }
                    }
                }
            }
            return when.all(tasks);
        }

        function mergeCut(self, cutName, srcManifest, dstManifest, and) {
            /*
            l"adjectives_verbs_nouns": { //a cut
              c"parcels": [              //cut parcels (grouping an arbitrary number of dictionary entries ordered by popularity)
                {
                  c"path": "adjectives_verbs_nouns/data00000000.json"
                  o"timestamp" : long    //personal only, when was this parcel saved
                  o"scores" : {...}      //personal only, how is this parcel scored, see below
                },
                ...
            */
            var tasks = [];
            var srcCut = srcManifest.manifest.cuts[cutName];
            var dstCut = dstManifest.manifest.cuts[cutName];
            if (and && !dstCut) {
                dstCut = dstManifest.manifest.cuts[cutName] = {
                    parcels : []
                };
            }
            if (dstCut) {
                for (var s = 0; s < srcCut.parcels.length; s++) {
                    var srcParcel = new Parcel(self, cutName, s, srcManifest);
                    var dstParcel = new Parcel(self, cutName, s, dstManifest);

                    function mergeParcelTask(src, dst) {
                        dstTask = undefined;
                        if (dst.exists()) {
                            dstTask = dst.load();
                        } else if (and) {
                            dstTask = true;
                        }
                        return $.when(src.load(), dstTask)
                        .then(function() {
                            return dst.merge(src, true);
                        });
                    }

                    tasks.push(mergeParcelTask(srcParcel, dstParcel));
                }
            }
            return when.all(tasks);
        }

        return PersistentLanguage;
    })();








    var Parcel = ( function() {
        function Parcel(languageObject, cutName, parcelIndex, parentManifestSource) {
            this.mLanguageObject        = languageObject;
            this.mCutName               = cutName;
            this.mIndex                 = parcelIndex;
            this.mParentManifestSource  = parentManifestSource;
        }

        Parcel.prototype.exists = function() {
            if (!this.mParentManifestSource) {
                var personalParcelManifest = exists(this.mLanguageObject.mManifests.personal, this.mCutName, this.mIndex);
                if (personalParcelManifest) {
                    this.mParentManifestSource = this.mLanguageObject.mManifests.personal;
                    this.mParcelManifest = personalParcelManifest;
                    return true;
                }
                var globalParcelManifest = exists(this.mLanguageObject.mManifests.global, this.mCutName, this.mIndex);
                if (globalParcelManifest) {
                    this.mParentManifestSource = this.mLanguageObject.mManifests.global;
                    this.mParcelManifest = globalParcelManifest;
                    return true;
                }
            } else {
                this.mParcelManifest = exists(this.mParentManifestSource, this.mCutName, this.mIndex);
                return !!this.mParcelManifest;
            }
            return false;
        }

        function exists(inManifest, cutName, parcelIndex) {
            if (inManifest &&
                inManifest.manifest &&
                inManifest.manifest.cuts &&
                cutName in inManifest.manifest.cuts &&
                inManifest.manifest.cuts[cutName].parcels[parcelIndex]) {
                return inManifest.manifest.cuts[cutName].parcels[parcelIndex];
            }
            return undefined;
        }

        Parcel.prototype.path = function() {
            return this.mLanguageObject.path(this.mParcelManifest.path);
        }

        Parcel.prototype.load = function() {
            var self = this;
            if (this.exists()) {
                return this.mLanguageObject.readFile(this.path(), this.mParentManifestSource.fs)
                .then(function(jsonData) {
                    self.mData = jsonData;
                });
            }
            return $.Deferred().reject(new Error("Parcel "+this.mCutName+"["+this.mIndex+"] not available")).promise();
        }

        Parcel.prototype.save = function(onlyLocal) {
            var tasks = [];
            if (this.mDirty) {
                var personalCut = this.mLanguageObject.mManifests.personal.manifest.cuts[this.mCutName];
                if (!personalCut) {
                    personalCut = this.mLanguageObject.mManifests.personal.manifest.cuts[this.mCutName] = {
                        parcels : []
                    };
                }
                var i = 0;
                for (; i < this.mIndex; i++) {
                    if (!personalCut.parcels[i]) {
                        personalCut.parcels[i] = null;
                    }
                }
                this.mParcelManifest.sessionId = this.mLanguageObject.mManifests.personal.manifest.sessionId;
                personalCut.parcels[i] = this.mParcelManifest;

                var stringifiedData = JSON.stringify(this.mData);
                tasks.push(this.mLanguageObject.mManifests.personal.fs.write(this.path(), stringifiedData));
                if (this.mLanguageObject.mManifests.personalRemote && !onlyLocal) {
                    tasks.push(this.mLanguageObject.mManifests.personalRemote.fs.write(this.path(), stringifiedData));
                }
                delete this["mDirty"];
            }
            return when.all(tasks);
        }

        Parcel.prototype.size = function(saveManifest) {
            if (this.mParcelManifest && this.mParcelManifest.scores) {
                return this.mParcelManifest.scores;
            }
            return 0;
        }

        Parcel.prototype.merge = function (otherParcel, saveParcel) {

            function compare(self, him) {
                if (self.exists()) {
                    return him.mParentManifestSource.manifest.timestamp - self.mParentManifestSource.manifest.timestamp;
                } else {
                    return -1;
                }
            }

            var compareResult = compare(this, otherParcel);
            if ( compareResult ) {
                var mergeResult = undefined;
                if (compareResult > 0) {
                    mergeResult = mergeEntries(this.mData, otherParcel.mData, false);
                } else {
                    mergeResult = mergeEntries(otherParcel.mData, this.mData, false);
                }
                if (!this.mParcelManifest) {
                    debug.assert(!this.mParentManifestSource.manifest.cuts[this.mCutName].parcels[this.mIndex],
                        "Parcel appears only just created, yet the parent seems to know about it already");
                    this.mParcelManifest = {
                        path : otherParcel.mParcelManifest.path
                    }
                    for (var i = 0; i <= this.mIndex; i++) {
                        if (i == this.mIndex) {
                            this.mParentManifestSource.manifest.cuts[this.mCutName].parcels.push(this.mParcelManifest);
                        }
                        else if (i > this.mParentManifestSource.manifest.cuts[this.mCutName].parcels.length) {
                            self.mCachedCuts[cutName].parcels.push(null);
                        }
                    }
                }
                //a new parcel has been cooked
                this.mDirty                 = true;
                this.mLanguageObject.mDirty = true;
                this.mData                  = mergeResult.entries;
                this.mParcelManifest.scores = mergeResult.scores;

                if (saveParcel) {
                    this.save();
                }
            }
        }

        function mergeEntries(refEntries, auxEntries, keepAbsentAuxEntries) {
            var fileScores = {};
            var resultEntries = {};
            if (keepAbsentAuxEntries) {
                resultEntries = auxEntries;
            }

            var entries = 0;
            for (var entryKey in refEntries) {
                entries++;
                var mergedEntry = refEntries[entryKey];
                if (auxEntries && (entryKey in auxEntries)) {
                    mergedEntry = mergeEntries(mergedEntry, auxEntries[entryKey]);
                }
                resultEntries[entryKey] = mergedEntry;
                fileScores = mergeScores(fileScores, mergedEntry.scores);
            }
            return {
                entries : resultEntries,
                score   : fileScores
            };
        }





        function mergeEntry(ref, aux) {
            /*
            A single entry in a file is expected to be as follows:
            where prefixes mean:
            c: compulsory
            l: example (of a literal)
            o: optional

            cl"una" : {                     //base word entry
              o"scores" : {                 //(for personal files only) user's score in various presentations
                l"basic" {                  //presentation example (here: ask basic meaning)
                    c"score"       : 0.22,  //0..1 score - 1-bad record, 0-best record (100% accurate score)
                    c"impressions   : 11    //how many times was this entry presented to calculate the weight
                }
              }
              c"variants": {                //all its variations/manifestations (as a Verb, Noun, etc)
                e"Adjective": {
                  "english": [
                    "Feminine of uno"
                  ]
                },
                e"Noun": {
                  "english": [
                    "Feminine of uno"
                  ]
                }
              }
            },
            */
            result = {
                variants : ref.variants,
                scores   : mergeScores(ref.scores, aux ? aux.scores : undefined)
            }


            return result;
        }

        return Parcel;
    })();







    /*
    A Language Object - presents the current working dictionary for a specific langauge
    */
    var Language = ( function() {

        var KDefaultWordsetLoading      = 20;
        var KDefaultSaveLocalEvery      = 5;
        var KDefaultSaveRemoteEvery     = 20;
        var KDefaultCut                 = "adjectives_verbs_nouns";
        var KDefaultScore               = "basic";
        var KMaxWeightChangeFactor      = 0.3;
        function Language(languages, path) {
            this.mLanguages = languages;
            PersistentLanguage.call(this, languages.mFileSystem, path, languages.sessionId());
        }
        Language.prototype.__proto__ = PersistentLanguage.prototype;


        Language.prototype.begin = function(params) {
            this.mImpressions = 0;
            this.mArrayCursor = 0;
            this.mWorkingSetArray = [];
            this.mWorkingSetDict  = {};
            this.mParams = params;
            if (!this.mParams) {
                this.mParams = {};
            }
            var self = this;
            if (!this.mParams.loading) {
                this.mParams.loading = KDefaultWordsetLoading;
            }
            if (!this.mParams.stepFunction) {
                this.mParams.stepFunction = function() {
                    return Math.random();
                }
            }
            if (!this.mParams.saveLocalEveryImpressions) {
                this.mParams.saveLocalEveryImpressions = KDefaultSaveLocalEvery;
            }
            if (!this.mParams.saveRemoteEveryLocalSaves) {
                this.mParams.saveRemoteEveryLocalSaves = KDefaultSaveRemoteEvery;
            }
            if (!this.mParams.cut) {
                this.mParams.cut=KDefaultCut;
            }
            if (!this.mParams.score) {
                this.mParams.score=KDefaultScore;
            }
            return ensureSaturation(this);
        }

        Language.prototype.loading = function() {
            var loading = 0;
            for (var i = 0; i < this.mWorkingSetArray.length; i++ ) {
                var elem = this.mWorkingSetArray[i];
                loading += elem.properties.scores[this.mParams.score].score;
            }
            return loading;
        }

        function ensureSaturation(self) {
            var missingLoading = self.mParams.loading-self.loading(self);
            if (missingLoading > 0) {
                return getParcel(self)
                .then(function(parcel) {
                    for (var word in parcel.mData) {
                        if (!(word in self.mWorkingSetDict)) {
                            var properties = parcel.mData[word];
                            if (!properties.scores) {
                                properties.scores = {};
                            }
                            if (!properties.scores[self.mParams.score]) {
                                properties.scores[self.mParams.score] = {
                                    impressions : 0,
                                    score       : 1
                                }
                            }
                            self.mWorkingSetDict[word] = true;
                            self.mWorkingSetArray.push({
                                word        : word,
                                properties  : properties,
                                fromParcel  : parcel
                            });
                            missingLoading -= properties.scores[self.mParams.score].score;
                            if (missingLoading <= 0) {
                                break;
                            }
                        }
                    }
                });
            }
        }

        function getParcel(self) {
            //TODO, this just works with the first parcel for now
            //expand with parcel advancement and retirement
            if (self.mCurrentParcel) {
                return $.Deferred().resolve(self.mCurrentParcel).promise();
            }
            return self.getParcel(self.mParams.cut, 0)
            .then(function(parcel) {
                return self.mCurrentParcel = parcel;
            });
        }

        Language.prototype.end = function(params) {
        }

        Language.prototype.nextWord = function(params) {
            var self = this;
            var stride = this.mParams.stepFunction();
            var entry = undefined;
            while (stride > 0) {
                entry = this.mWorkingSetArray[this.mArrayCursor];
                stride -= entry.properties.scores[this.mParams.score].score;
                this.mArrayCursor = ++this.mArrayCursor % this.mWorkingSetArray.length;
            }



            entry.properties.scores[this.mParams.score].impressions++;
            return {
                entry        : entry,

                /*
                0..1 where 0 - not guessed, 1 guessed correctly
                */
                feedback    : function(result) {
                    entry.fromParcel.mDirty = true;
                    self.mDirty = true;
                    result = result*2-1;
                    entry.properties.scores[self.mParams.score].score = Math.min(1,
                        entry.properties.scores[self.mParams.score].score-
                        KMaxWeightChangeFactor*result*entry.properties.scores[self.mParams.score].score);
                    ensureSaturation(self);
                    if ((++self.mImpressions % self.mParams.saveLocalEveryImpressions)==0) {
                        var onlyLocal = true;
                        if (((self.mImpressions/self.mParams.saveLocalEveryImpressions) % self.mParams.saveRemoteEveryLocalSaves) == 0) {
                            onlyLocal = false;
                        }
                        self.save(onlyLocal);
                    }
                }
            };
        }


        var KMaxWidthPx = 7;
        var KColours = [
            '#FFFACD', //lemonchiffon
            '#FF8C00', //darkorange
            '#1E90FF'  //dodgerblue
        ];
        var KDiagnostics = "<span style=\"display:inline-block;width:%WIDTH%px;height:5px;background-color:%BACKGROUND_COLOR%;\"/>"
        var KStatus = "<div id class=\"ui-dialog-content ui-widget-content\" style=\"background: none; border: 0; line-height:0;\">%STATUS%</div>"
        function redrawStatus(self) {
            if (self.mParams.diagnostics) {
                var statusElement = "";
                var knownWords = 0;
                var lastColourIndex = 0;
                for (var i = 0; i < self.mWorkingSet.length; i++) {
                    var word = self.mWorkingSet[i];
                    if ("colourIdx" in word) {
                        lastColourIndex = word.colourIdx;
                    } else {
                        lastColourIndex++;
                        lastColourIndex %= KColours.length;
                        word.colourIdx = lastColourIndex;
                    }
                    statusElement += KDiagnostics.replace("%WIDTH%", KMaxWidthPx*word.weight).replace("%BACKGROUND_COLOR%", KColours[word.colourIdx]);
                    knownWords += 1-word.weight;
                }
                knownWords = Math.floor(knownWords*100)/100;
                statusElement = KStatus.replace("%STATUS%", "Known words: "+knownWords) + statusElement;
                self.mParams.diagnostics.empty();
                self.mParams.diagnostics.append(statusElement);
            }
        }

        return Language;
    })();

    return {Language : Language};
})();



//for node based tests
if (typeof window === 'undefined') {
    module.exports = language;
}


